Dynamic Programming Patterns
Recognizing and Formulating Dynamic Programming Problems
Now that you understand how to implement Dynamic Programming (DP) using Memoization and Tabulation, the next step is learning how to recognize when a problem can be solved using DP and how to systematically formulate a DP solution.
How to Recognize DP Problems
Dynamic Programming is applicable when a problem exhibits the following characteristics:
-
Optimal Substructure
- The solution to a larger problem can be constructed from solutions to smaller subproblems.
- Example: The Fibonacci sequence is built using smaller Fibonacci numbers:
.
-
Overlapping Subproblems
- The same subproblems are solved multiple times in a naive approach.
- Example: In a naive recursive Fibonacci implementation, is computed multiple times while solving .
Example Problem: 0/1 Knapsack Problem
Problem Statement:
You are given items, each with a weight and value. Your task is to determine the maximum total value you can obtain by selecting a subset of the items, such that the total weight does not exceed the knapsack's capacity . Each item can be included at most once.
Steps to Formulate a DP Solution
-
Define the Problem State
- Identify the variables that represent the problem's state at any given point.
- Example: In the Knapsack Problem, the state is represented as , where:
- : The index of the current item.
- : The remaining capacity of the knapsack.
-
Define the Recurrence Relation
- Establish how the solution to a problem depends on solutions to its subproblems.
- Example: In the Knapsack Problem, the recurrence relation is: This means that for each item, we decide whether to include it in the knapsack (if weight allows) or exclude it.
-
Identify Base Cases
- Determine the simplest scenarios where the solution is known without computation.
- Example: In the Knapsack Problem, no items mean zero value:
.
-
Iterate (Tabulation) or Recurse (Memoization)
- Use the recurrence relation to compute subproblem values either iteratively or recursively.
-
Extract the Final Answer
- The final answer is typically stored in a specific entry in the table or memoization structure.
- Example: For the Knapsack Problem, the solution is in , where is the total number of items and is the knapsack capacity.
Formulating the DP Solution
-
State Representation
- Let represent the maximum value obtainable using the first items with a knapsack capacity .
-
Recurrence Relation
- If the -th item is not included:
- If the -th item is included (and its weight fits in the knapsack):
- Combine both cases:
-
Base Cases
- : No items mean no value for any capacity.
- : A knapsack with zero capacity cannot hold any items.
-
Final Answer
- The solution is stored in .
Python Implementation
def knapsack(weights, values, W):
"""
Solve the 0/1 Knapsack Problem.
Args:
weights (list): The weights of the items.
values (list): The values of the items.
W (int): The maximum weight capacity of the knapsack.
Returns:
int: The maximum value obtainable.
"""
n = len(weights)
# Initialize DP table
dp = [[0] * (W + 1) for _ in range(n + 1)]
# Populate the DP table
for i in range(1, n + 1):
for w in range(W + 1):
if weights[i - 1] <= w:
# Include the item or exclude it
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1])
else:
# Exclude the item
dp[i][w] = dp[i - 1][w]
return dp[n][W]
# Example usage
weights = [1, 2, 3] # Weights of items
values = [6, 10, 12] # Values of items
W = 5 # Knapsack capacity
print(knapsack(weights, values, W)) # Output: 22
Explanation of Example
-
Input:
- Weights: [1, 2, 3] (weights of the items).
- Values: [6, 10, 12] (values of the items).
- Capacity: .
-
Process:
- Use dynamic programming to calculate the maximum value for each subproblem.
- For capacity , including items 2 and 3 yields a total value of 22.
-
Output:
- The maximum value obtainable is 22.
Java Implementation
public class Knapsack {
public static int knapsack(int[] weights, int[] values, int W) {
int n = weights.length;
// Initialize DP table
int[][] dp = new int[n + 1][W + 1];
// Populate DP table
for (int i = 1; i <= n; i++) {
for (int w = 0; w <= W; w++) {
if (weights[i - 1] <= w) {
// Include the item or exclude it
dp[i][w] = Math.max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]);
} else {
// Exclude the item
dp[i][w] = dp[i - 1][w];
}
}
}
return dp[n][W];
}
public static void main(String[] args) {
int[] weights = {1, 2, 3}; // Weights of items
int[] values = {6, 10, 12}; // Values of items
int W = 5; // Knapsack capacity
System.out.println(knapsack(weights, values, W)); // Output: 22
}
}
Optimized Space Complexity
Since the current state in the 0/1 Knapsack problem only depends on the previous state, we can reduce space complexity to by using a 1D array.
Python (Space-Optimized):
def knapsack_optimized(weights, values, W):
n = len(weights)
dp = [0] * (W + 1)
for i in range(n):
for w in range(W, weights[i] - 1, -1): # Traverse backwards
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[W]
# Example usage
weights = [1, 2, 3]
values = [6, 10, 12]
W = 5
print(knapsack_optimized(weights, values, W)) # Output: 22
Java (Space-Optimized):
public class KnapsackOptimized {
public static int knapsack(int[] weights, int[] values, int W) {
int n = weights.length;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
for (int w = W; w >= weights[i]; w--) { // Traverse backwards
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[W];
}
public static void main(String[] args) {
int[] weights = {1, 2, 3};
int[] values = {6, 10, 12};
int W = 5;
System.out.println(knapsack(weights, values, W)); // Output: 22
}
}
Advanced Dynamic Programming Patterns
Once you’re comfortable with the basics of DP, it’s time to explore more complex and widely applicable patterns. These include multi-dimensional DP problems, problems with sequences, and problems involving multiple decisions at each step. Here are some advanced DP topics:
1. Longest Common Subsequence (LCS)
Problem Statement
Given two strings, find the length of their longest subsequence that appears in both strings. A subsequence is a sequence that appears in the same order but not necessarily consecutively.
Formulation
-
State Representation
Let represent the length of the LCS of the first characters of string and the first characters of string . -
Recurrence Relation
- If the characters match:
- If the characters don’t match:
-
Base Cases
- : LCS of an empty string and any other string is 0.
- : LCS of any string and an empty string is 0.
-
Final Answer
, where and are the lengths of the strings.
Implementation
Python Code:
def longest_common_subsequence(A, B):
m, n = len(A), len(B)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if A[i - 1] == B[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
# Example usage
print(longest_common_subsequence("ABCBDAB", "BDCAB")) # Output: 4
Java Code:
public class LCS {
public static int longestCommonSubsequence(String A, String B) {
int m = A.length(), n = B.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (A.charAt(i - 1) == B.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
System.out.println(longestCommonSubsequence("ABCBDAB", "BDCAB")); // Output: 4
}
}
2. Matrix Chain Multiplication
Problem Statement
Given dimensions of matrices, determine the minimum number of scalar multiplications required to multiply them in an optimal order.
Formulation
-
State Representation
Let represent the minimum cost to multiply matrices from to . -
Recurrence Relation
- To multiply matrices from to , try splitting at :
- Cost of multiplication at :
-
Base Cases
: A single matrix requires no multiplication. -
Final Answer
, where is the number of matrices.
Implementation
Python Code:
def matrix_chain_multiplication(dimensions):
n = len(dimensions)
dp = [[0] * n for _ in range(n)]
for length in range(2, n):
for i in range(1, n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')
for k in range(i, j):
cost = dp[i][k] + dp[k + 1][j] + dimensions[i - 1] * dimensions[k] * dimensions[j]
dp[i][j] = min(dp[i][j], cost)
return dp[1][n - 1]
# Example usage
dimensions = [10, 20, 30, 40, 30]
print(matrix_chain_multiplication(dimensions)) # Output: 30000
Java Code:
public class MatrixChainMultiplication {
public static int matrixChainMultiplication(int[] dimensions) {
int n = dimensions.length;
int[][] dp = new int[n][n];
for (int length = 2; length < n; length++) {
for (int i = 1; i < n - length + 1; i++) {
int j = i + length - 1;
dp[i][j] = Integer.MAX_VALUE;
for (int k = i; k < j; k++) {
int cost = dp[i][k] + dp[k + 1][j] + dimensions[i - 1] * dimensions[k] * dimensions[j];
dp[i][j] = Math.min(dp[i][j], cost);
}
}
}
return dp[1][n - 1];
}
public static void main(String[] args) {
int[] dimensions = {10, 20, 30, 40, 30};
System.out.println(matrixChainMultiplication(dimensions)); // Output: 30000
}
}
3. Subset Sum Problem
Problem Statement
Given a set of integers and a target sum, determine if there is a subset whose sum equals the target.
Formulation
-
State Representation
Let represent whether it’s possible to achieve sum using the first elements. -
Recurrence Relation
- If the current element is not included:
- If the current element is included:
- If the current element is not included:
-
Base Cases
- : A sum of 0 is always possible with no elements.
- : Sum 0 is possible with any set of elements.
-
Final Answer
, where is the size of the set.
Implementation
Python Code:
def subset_sum(nums, target):
n = len(nums)
dp = [[False] * (target + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp[i][0] = True
for i in range(1, n + 1):
for j in range(1, target + 1):
if nums[i - 1] > j:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]]
return dp[n][target]
# Example usage
nums = [3, 34, 4, 12, 5, 2]
target = 9
print(subset_sum(nums, target)) # Output: True
Java Code:
public class SubsetSum {
public static boolean subsetSum(int[] nums, int target) {
int n = nums.length;
boolean[][] dp = new boolean[n + 1][target + 1];
for (int i = 0; i <= n; i++) {
dp[i][0] = true;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= target; j++) {
if (nums[i - 1] > j) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j
] || dp[i - 1][j - nums[i - 1]];
}
}
}
return dp[n][target];
}
public static void main(String[] args) {
int[] nums = {3, 34, 4, 12, 5, 2};
int target = 9;
System.out.println(subsetSum(nums, target)); // Output: true
}
}
Specialized Optimizations in Dynamic Programming
As we advance, some problems require highly optimized approaches to manage constraints such as memory usage, time complexity, or complex problem structures like graphs and combinatorics. This section explores Bitmask DP, Digit DP, and Graph-based DP techniques.
1. Bitmask DP
Problem Statement
You are given tasks, each requiring one unique worker from workers. Each worker has a specific cost for performing a task. Find the minimum cost to assign all tasks such that every task is assigned to a unique worker.
Formulation
-
State Representation
Use a bitmask to represent which tasks have been assigned. If is the minimum cost to complete tasks represented by the bitmask , the state depends on the tasks remaining. -
Recurrence Relation
For each worker , iterate through all tasks :- If task is not assigned in , calculate the cost by assigning to :
-
Base Case
: No cost if no tasks are assigned. -
Final Answer
: The bitmask where all tasks are assigned.
Implementation
Python Code:
def min_cost_assignment(cost):
n = len(cost)
dp = [float('inf')] * (1 << n)
dp[0] = 0
for mask in range(1 << n):
num_assigned = bin(mask).count('1')
for task in range(n):
if not (mask & (1 << task)): # If task is not assigned
dp[mask | (1 << task)] = min(dp[mask | (1 << task)],
dp[mask] + cost[num_assigned][task])
return dp[(1 << n) - 1]
# Example usage
cost = [
[9, 2, 7],
[6, 4, 3],
[5, 8, 1]
]
print(min_cost_assignment(cost)) # Output: 13
Java Code:
public class BitmaskDP {
public static int minCostAssignment(int[][] cost) {
int n = cost.length;
int[] dp = new int[1 << n];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int mask = 0; mask < (1 << n); mask++) {
int numAssigned = Integer.bitCount(mask);
for (int task = 0; task < n; task++) {
if ((mask & (1 << task)) == 0) { // If task is not assigned
dp[mask | (1 << task)] = Math.min(dp[mask | (1 << task)],
dp[mask] + cost[numAssigned][task]);
}
}
}
return dp[(1 << n) - 1];
}
public static void main(String[] args) {
int[][] cost = {
{9, 2, 7},
{6, 4, 3},
{5, 8, 1}
};
System.out.println(minCostAssignment(cost)); // Output: 13
}
}
2. Digit DP
Problem Statement
Count numbers between and that satisfy a specific property (e.g., digits sum up to a given value, no repeating digits, divisible by ).
Formulation
-
State Representation
: The number of valid numbers formed considering up to the -th digit, whether the number is still "tight" (matches the prefix of ), and the state variable (e.g., digit sum or used digits). -
Recurrence Relation
- For each digit :
- If , is constrained by the current digit of .
- Update the state and recurse for the next digit.
- For each digit :
-
Base Case
If , return whether the state satisfies the condition. -
Final Answer
Subtract results from and .
Implementation
Python Code:
def digit_dp(pos, tight, sum_, digits, dp):
if pos == len(digits):
return 1 if sum_ % 2 == 0 else 0 # Example condition: sum of digits is even
if dp[pos][tight][sum_] != -1:
return dp[pos][tight][sum_]
limit = digits[pos] if tight else 9
result = 0
for digit in range(0, limit + 1):
result += digit_dp(pos + 1, tight and (digit == limit), sum_ + digit, digits, dp)
dp[pos][tight][sum_] = result
return result
def count_numbers(L, R):
def preprocess(num):
return list(map(int, str(num)))
def solve(num):
digits = preprocess(num)
dp = [[[-1] * 100 for _ in range(2)] for _ in range(len(digits))]
return digit_dp(0, 1, 0, digits, dp)
return solve(R) - solve(L - 1)
# Example usage
print(count_numbers(10, 20)) # Output: Numbers between 10 and 20 with even digit sums.
Java Code:
public static int minCostAssignment(int[][] cost) {
int n = cost.length;
int[] dp = new int[1 << n];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int mask = 0; mask < (1 << n); mask++) {
int numAssigned = Integer.bitCount(mask);
for (int task = 0; task < n; task++) {
if ((mask & (1 << task)) == 0) { // If task is not assigned
dp[mask | (1 << task)] = Math.min(dp[mask | (1 << task)],
dp[mask] + cost[numAssigned][task]);
}
}
}
return dp[(1 << n) - 1];
}
public static void main(String[] args) {
int[][] cost = {
{9, 2, 7},
{6, 4, 3},
{5, 8, 1}
};
System.out.println(minCostAssignment(cost)); // Output: 13
}
}
3. Graph-based DP
Problem Statement
Find the shortest path from a source to all vertices in a graph with weighted edges.
Formulation
-
State Representation
: The shortest path to vertex using at most edges. -
Recurrence Relation
-
Base Case
, for all